package org.example.system.service.impl;

import cn.hutool.captcha.AbstractCaptcha;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.jwt.JWTPayload;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.system.common.constant.SecurityConstants;
import org.example.system.common.enums.CaptchaTypeEnum;
import org.example.system.model.dto.CaptchaResult;
import org.example.system.model.dto.LoginResult;
import org.example.system.plugin.captcha.CaptchaProperties;
import org.example.system.security.util.JwtUtils;
import org.example.system.service.AuthService;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.awt.*;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * 认证服务实现类
 *
 * @author LY
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class AuthServiceImpl implements AuthService {

    private static final String TOKEN_TYPE = "Bearer";

    private final AuthenticationManager authenticationManager;
    private final RedisTemplate<String, Object> redisTemplate;
    private final CaptchaProperties captchaProperties;
    private final Font captchaFont;
    private final CodeGenerator codeGenerator;

    /**
     * 重写登录方法，用于用户认证和登录
     *
     * @param username 用户名，需要进行空格去除和小写转换以统一输入格式
     * @param password 密码，用于与用户名一起进行认证
     * @return 登录成功后返回包含accessToken的LoginResult对象
     * @throws IllegalArgumentException 如果用户名或密码为空
     * @throws AuthenticationException    如果认证失败
     */
    @Override
    public LoginResult login(String username, String password) {
        // 参数校验
        if (username == null || username.trim().isEmpty()) {
            throw new IllegalArgumentException("Username cannot be empty");
        }
        if (password == null || password.isEmpty()) {
            throw new IllegalArgumentException("Password cannot be empty");
        }

        // 记录用户登录尝试的日志，使用占位符避免潜在的日志注入问题
        log.debug("Attempting login for user: {}", username);

        // 创建认证令牌，对用户名进行格式化处理以统一输入
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(username.toLowerCase().trim(), password.trim());

        // 使用认证管理器进行用户认证
        Authentication authentication;
        try {
            authentication = authenticationManager.authenticate(authenticationToken);
        } catch (AuthenticationException e) {
            log.warn("Authentication failed for user: {}", username);
            throw e; // 可以替换为自定义异常封装
        }

        // 生成JWT令牌
        String accessToken = JwtUtils.generateToken(authentication);

        // 记录用户成功登录的日志
        log.info("User {} logged in successfully", username);

        // 从生成的 accessToken 解析出 JWT 的过期时间戳
        Map<String, Object> claims = JwtUtils.parseToken(accessToken);
        Long expiration = Convert.toLong(claims.get(JWTPayload.EXPIRES_AT));

        if (expiration == null) {
            throw new RuntimeException("Failed to get token expiration time");
        }

        // 计算剩余存活时间（秒）
        long ttl = expiration - System.currentTimeMillis() / 1000;

        String onlineKey = SecurityConstants.ONLINE_USER_PREFIX + username;
        redisTemplate.opsForValue().set(onlineKey, accessToken, ttl, TimeUnit.SECONDS);

        // 构建并返回登录结果对象，包含令牌类型和访问令牌
        return LoginResult.builder()
                .tokenType(TOKEN_TYPE)
                .accessToken(accessToken)
                .build();
    }


    /**
     * 退出登录功能实现
     * 本方法通过获取HTTP请求中的授权令牌，进行解析和验证，并在用户退出时将令牌加入黑名单
     * 以确保用户退出后无法再使用该令牌访问受保护的资源
     */
    @Override
    public void logout() {
        // 获取当前请求的HttpServletRequest对象
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(
                RequestContextHolder.getRequestAttributes())).getRequest();

        // 从请求头中获取授权令牌
        String token = request.getHeader(HttpHeaders.AUTHORIZATION);

        // 如果退出请求中未找到令牌，则清除安全上下文并返回
        if (StrUtil.isBlank(token)) {
            log.debug("No token found in logout request");
            SecurityContextHolder.clearContext();
            return;
        }

        // 解析令牌以获取声明
        Map<String, Object> claims = JwtUtils.parseToken(token);
        // 如果令牌格式无效，则清除安全上下文并返回
        if (claims == null) {
            log.warn("Invalid token format during logout");
            SecurityContextHolder.clearContext();
            return;
        }

        // 获取JWT的唯一标识符（JWT ID）
        String jti = Convert.toStr(claims.get(JWTPayload.JWT_ID));
        // 获取令牌的过期时间戳
        Long expiration = Convert.toLong(claims.get(JWTPayload.EXPIRES_AT));

        // 如果过期时间存在，则计算令牌剩余的有效时间，并将令牌加入黑名单
        if (expiration != null) {
            long ttl = expiration - System.currentTimeMillis() / 1000;
            if (ttl > 0) {
                redisTemplate.opsForValue().set(
                        SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti,
                        "",
                        ttl,
                        TimeUnit.SECONDS
                );
                log.debug("Token blacklisted with TTL: {} seconds", ttl);
            }
        } else {
            // 如果过期时间不存在，则直接将令牌加入黑名单，不设置过期时间
            redisTemplate.opsForValue().set(
                    SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti,
                    ""
            );
            log.debug("Token blacklisted without TTL");
        }

        // 清除安全上下文，完成退出登录操作
        SecurityContextHolder.clearContext();
        log.info("User logged out successfully");
    }

    /**
     * 生成验证码并返回验证码结果对象
     * 此方法首先创建一个验证码实例，然后设置验证码的各种属性，如文本透明度和字体
     * 随后，它生成验证码代码和键，并将验证码代码与键的映射存储在Redis中
     * 最后，它构建并返回一个包含验证码键和Base64编码的验证码图片的验证码结果对象
     *
     * @return CaptchaResult 验证码结果对象，包含验证码键和Base64编码的验证码图片
     */
    @Override
    public CaptchaResult getCaptcha() {
        // 创建验证码实例
        AbstractCaptcha captcha = createCaptcha();
        // 设置验证码代码生成器
        captcha.setGenerator(codeGenerator);
        // 设置文本透明度
        captcha.setTextAlpha(captchaProperties.getTextAlpha());
        // 设置字体
        captcha.setFont(captchaFont);

        // 获取生成的验证码代码
        String captchaCode = captcha.getCode();
        // 生成验证码键
        String captchaKey = IdUtil.fastSimpleUUID();

        // 将验证码代码与键的映射存储在Redis中，并设置过期时间
        redisTemplate.opsForValue().set(
                SecurityConstants.CAPTCHA_CODE_PREFIX + captchaKey,
                captchaCode,
                captchaProperties.getExpireSeconds(),
                TimeUnit.SECONDS
        );

        // 记录日志，调试信息，包含生成的验证码键
        log.debug("Generated captcha with key: {}", captchaKey);

        // 构建并返回验证码结果对象
        return CaptchaResult.builder()
                .captchaKey(captchaKey)
                .captchaBase64(captcha.getImageBase64Data())
                .build();
    }

    /**
     * 创建验证码实例
     * 此方法根据配置属性创建不同类型的验证码它从配置中获取验证码的类型、大小、干扰元素数量和代码长度，
     * 然后根据验证码类型调用相应的工具方法来生成验证码实例
     *
     * @return AbstractCaptcha 返回一个验证码实例
     */
    private AbstractCaptcha createCaptcha() {
        // 获取验证码类型
        String captchaType = captchaProperties.getType();
        // 获取验证码宽度
        int width = captchaProperties.getWidth();
        // 获取验证码高度
        int height = captchaProperties.getHeight();
        // 获取干扰元素数量
        int interfereCount = captchaProperties.getInterfereCount();
        // 获取验证码代码长度
        int codeLength = captchaProperties.getCode().getLength();

        // 根据验证码类型创建相应的验证码实例
        return switch (CaptchaTypeEnum.valueOf(captchaType.toUpperCase())) {
            // 创建圆形验证码
            case CIRCLE -> CaptchaUtil.createCircleCaptcha(width, height, codeLength, interfereCount);
            // 创建GIF验证码
            case GIF -> CaptchaUtil.createGifCaptcha(width, height, codeLength);
            // 创建线条验证码
            case LINE -> CaptchaUtil.createLineCaptcha(width, height, codeLength, interfereCount);
            // 创建剪切验证码
            case SHEAR -> CaptchaUtil.createShearCaptcha(width, height, codeLength, interfereCount);
        };
    }
}